//--------------------------------------------------------------------------------
// This file is part of the Windows Workflow Foundation Sample Code
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// This source code is intended only as a supplement to Microsoft
// Development Tools and/or on-line documentation.  See these other
// materials for detailed information regarding Microsoft code samples.
// 
// THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//--------------------------------------------------------------------------------

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.ComponentModel.Design.Serialization;
using System.Collections;
using System.Drawing.Design;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using System.Workflow.ComponentModel;
using System.Workflow.Runtime;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Design;
using Microsoft.Samples.Workflow.UI.Design;

namespace Microsoft.Samples.Workflow.UI
{
    public enum SynchronizationMode
    {
        Allowed = 0,
        HistoryOnly = 1,
        NotAllowed = 2
    }

    [Designer(typeof(NavigatorDesigner), typeof(IDesigner))]
    [DesignerSerializer(typeof(NavigatorCodeDomSerializer), typeof(CodeDomSerializer))]
    [ActivityValidator(typeof(NavigatorValidator))]
    public class Navigator : CompositeActivity, IActivityEventListener<QueueEventArgs>, IActivityEventListener<ActivityExecutionStatusChangedEventArgs>
    {
        public static readonly DependencyProperty StartWithProperty = DependencyProperty.Register("StartWith", typeof(string), typeof(Navigator), new PropertyMetadata(null, DependencyPropertyOptions.Metadata));
        public static readonly DependencyProperty SynchronizationModeProperty = DependencyProperty.Register("SynchronizationMode", typeof(SynchronizationMode), typeof(Navigator), new PropertyMetadata(SynchronizationMode.Allowed, DependencyPropertyOptions.Metadata));
        public static readonly DependencyProperty NavigatingEvent = DependencyProperty.Register("Navigating", typeof(EventHandler<NavigatorEventArgs>), typeof(Navigator));
        public static readonly DependencyProperty TransitionsProperty = DependencyProperty.RegisterAttached("Transitions", typeof(TransitionCollection), typeof(Navigator), new PropertyMetadata(null, DependencyPropertyOptions.Metadata));
        public static readonly DependencyProperty OwnerPermissionProperty = DependencyProperty.Register("OwnerPermission", typeof(Permission), typeof(Navigator));
        public static readonly DependencyProperty EnforceOwnerPermissionProperty = DependencyProperty.Register("EnforceOwnerPermission", typeof(bool), typeof(Navigator), new PropertyMetadata(false, DependencyPropertyOptions.Metadata));

        protected static readonly DependencyProperty CurrentProperty = DependencyProperty.Register("Current", typeof(Activity), typeof(Navigator));
        protected static readonly DependencyProperty HistoryProperty = DependencyProperty.RegisterAttached("History", typeof(List<Activity>), typeof(Navigator), new PropertyMetadata(DependencyPropertyOptions.NonSerialized));
        protected static readonly DependencyProperty HistoryContextIDsProperty = DependencyProperty.RegisterAttached("HistoryContextIDs", typeof(List<Guid>), typeof(Navigator));

        static readonly DependencyProperty ChildContextIDsProperty = DependencyProperty.Register("ChildContextIDs", typeof(List<Guid>), typeof(Navigator));

        // this field is set when SynchronizeTo is called.
        [NonSerialized]
        private Bookmark syncToBookmark = null;

        public Navigator() 
        {
        }

        public Navigator(string name)
            : base(name)
        {
        }

        [SRCategory(SR.Navigation)]
        [SRDescription(SR.StartWithDescription)]
        [Editor(typeof(StartWithEditor), typeof(UITypeEditor))]
        [ValidationOption(ValidationOption.Required)]
        [MergableProperty(false)]
        public string StartWith 
        {
            get 
            { 
                return base.GetValue(Navigator.StartWithProperty) as string; 
            }
            set 
            { 
                base.SetValue(Navigator.StartWithProperty, value); 
            }
        }

        [MergableProperty(false)]
        [SRCategory(SR.Navigation)]
        [SRDescription(SR.NavigatingHandlerDescription)]
        public event EventHandler<NavigatorEventArgs> Navigating 
        {
            add 
            {
                base.AddHandler(NavigatingEvent, value);
            }
            remove 
            {
                base.RemoveHandler(NavigatingEvent, value);
            }
        }

        [SRDescription(SR.SynchronizationModeDescription)]
        [SRCategory(SR.Navigation)]
        [MergableProperty(false)]
        public SynchronizationMode SynchronizationMode
        {
            get
            {
                return (SynchronizationMode)base.GetValue(SynchronizationModeProperty);
            }
            set
            {
                base.SetValue(SynchronizationModeProperty, value);
            }
        }

        [SRDescription(SR.OwnerPermissionDescription)]
        [SRCategory(SR.Activity)]
        [MergableProperty(false)]
        public Permission OwnerPermission
        {
            get
            {
                return (Permission)base.GetValue(OwnerPermissionProperty);
            }
            set
            {
                base.SetValue(OwnerPermissionProperty, value);
            }
        }

        [SRDescription(SR.EnforceOwnerPermissionDescription)]
        [SRCategory(SR.Activity)]
        [MergableProperty(false)]
        public bool EnforceOwnerPermission
        {
            get
            {
                return (bool)base.GetValue(EnforceOwnerPermissionProperty);
            }
            set
            {
                base.SetValue(EnforceOwnerPermissionProperty, value);
            }
        }

        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        [Browsable(false)]
        internal protected Activity Current
        {
            get
            {
                return (Activity)base.GetValue(CurrentProperty);
            }
            protected set
            {
                base.SetValue(CurrentProperty, value);
            }
        }

        // Property accessor for the "Transitions" attached property.
        public static object GetTransitions(object dependencyObject) 
        {
            Activity activity = dependencyObject as Activity;
            if (activity == null)
            {
                throw new ArgumentException(SR.GetString(SR.Error_TransitionsOnlyApplyToActivity));
            }

            TransitionCollection transitions = activity.GetValue(TransitionsProperty) as TransitionCollection;
            if (transitions == null && ((Navigator)activity.Parent).DesignMode)
            {
                transitions = new TransitionCollection();
                activity.SetValue(TransitionsProperty, transitions);
            }

            return transitions;
        }

        public static void SetTransitions(object dependencyObject, object value)
        {
            Activity activity = dependencyObject as Activity;
            if (activity == null)
            {
                throw new ArgumentException(SR.GetString(SR.Error_TransitionsOnlyApplyToActivity));
            }

            TransitionCollection transitions = value as TransitionCollection;
            if (transitions == null)
            {
                throw new ArgumentException(SR.GetString(SR.Error_UnexpectedArgumentType, typeof(TransitionCollection).FullName), "value");
            }

            activity.SetValue(TransitionsProperty, transitions);
        }

        protected override void Initialize(IServiceProvider provider)
        {
            if (this.EnforceOwnerPermission)
            {
                InitializeChildPermission();
            }

            base.Initialize(provider);

            if (this.Parent == null || this.IsDynamicActivity)
            {
                // setup the queue for command queries.
                WorkflowQueuingService queueService = provider.GetService(typeof(WorkflowQueuingService)) as WorkflowQueuingService;
                if (!queueService.Exists(this.Name))
                {
                    WorkflowQueue queue = queueService.CreateWorkflowQueue(this.Name, true);
                    queue.QueueItemAvailable += new EventHandler<QueueEventArgs>(OnQueueEvent);
                }
            }

            this.SetValue(ChildContextIDsProperty, new List<Guid>());
        }

        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) 
        {
            if (executionContext == null)
            {
                throw new ArgumentNullException("executionContext");
            }

            if (string.IsNullOrEmpty(this.StartWith))
            {
                throw new ArgumentNullException(SR.GetString(SR.Error_MissingStartWith, this.QualifiedName));
            }

            if (this.TryNavigatingTo(executionContext, null, new Bookmark(this.StartWith, Guid.Empty)))
            {
                return ActivityExecutionStatus.Executing;
            }

            return ActivityExecutionStatus.Closed;
        }

        protected override ActivityExecutionStatus Cancel(ActivityExecutionContext executionContext)
        {
            if (executionContext == null)
            {
                throw new ArgumentNullException("executionContext");
            }

            Activity currnent = this.Current;
            if (currnent != null)
            {
                if (currnent.ExecutionStatus == ActivityExecutionStatus.Executing)
                {
                    ActivityExecutionContext innerContext = executionContext.ExecutionContextManager.GetExecutionContext(currnent);
                    if (innerContext == null)
                    {
                        throw new NullReferenceException("innerContext");
                    }

                    innerContext.CancelActivity(currnent);
                    return ActivityExecutionStatus.Canceling;
                }
                else if (currnent.ExecutionStatus == ActivityExecutionStatus.Canceling || currnent.ExecutionStatus == ActivityExecutionStatus.Faulting)
                {
                    return ActivityExecutionStatus.Canceling;
                }
            }
            return ActivityExecutionStatus.Closed;
        }

        protected override void OnClosed(IServiceProvider provider)
        {
            // delete the command query queue
            WorkflowQueuingService queueService = provider.GetService(typeof(WorkflowQueuingService)) as WorkflowQueuingService;
            WorkflowQueue queue = queueService.GetWorkflowQueue(this.Name);
            Debug.Assert(queue != null);
            if (queue != null)
            {
                queue.QueueItemAvailable -= new EventHandler<QueueEventArgs>(OnQueueEvent);
                queueService.DeleteWorkflowQueue(this.Name);
            }

            base.OnClosed(provider);
        }

        void IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.OnEvent(object sender, ActivityExecutionStatusChangedEventArgs e)
        {
            ActivityExecutionContext context = sender as ActivityExecutionContext;
            if (context == null)
            {
                throw new InvalidOperationException("sender");
            }

            ActivityExecutionContext innerContext = context.ExecutionContextManager.GetExecutionContext(this.Current);
            if (innerContext == null)
            {
                throw new NullReferenceException("innerContext");
            }

            Navigator navigatorActivity = context.Activity as Navigator;
            if (navigatorActivity == null)
            {
                throw new NullReferenceException("context.Activity");
            }

            //first, unsubscribe to the inner context's activity
            Activity childActivity = innerContext.Activity;
            childActivity.UnregisterForStatusChange(Activity.ClosedEvent, this);
            //remove the inner context
            context.ExecutionContextManager.CompleteExecutionContext(innerContext, true);

            if (navigatorActivity.ExecutionStatus == ActivityExecutionStatus.Canceling || navigatorActivity.ExecutionStatus == ActivityExecutionStatus.Faulting)
            {
                CloseNavigator(context);
            }
            else if (navigatorActivity.ExecutionStatus == ActivityExecutionStatus.Executing)
            {
                if (e.ExecutionResult == ActivityExecutionResult.Canceled && this.syncToBookmark == null)
                {
                    CloseNavigator(context);
                }
                else
                {
                    if (!TryNavigatingTo(context, childActivity, this.syncToBookmark))
                    {
                        CloseNavigator(context);
                    }
                    else if (e.ExecutionResult == ActivityExecutionResult.Canceled && this.syncToBookmark != null)
                    {
                        // This means SynchronizeTo is called to go to a certain activity and we've successfully
                        // moved to that specified activity.  Fire the command completed event.
                        this.syncToBookmark = null;
                    }

                }
            }
        }

        void IActivityEventListener<QueueEventArgs>.OnEvent(object sender, QueueEventArgs e)
        {
            this.OnQueueEvent(sender, e);
        }

        void OnQueueEvent(object sender, QueueEventArgs e)
        {
            ActivityExecutionContext executionContext = sender as ActivityExecutionContext;
            WorkflowQueuingService queueService = executionContext.GetService<WorkflowQueuingService>();
            WorkflowQueue queue = queueService.GetWorkflowQueue(this.Name);

            if (queue.Count > 0)
            {
                object cmd = queue.Dequeue();

                this.ProcessCommand(cmd, executionContext);
            }
            else
            {
                throw new InvalidOperationException(SR.GetString(SR.Error_CantDequeueMessage, this.Name));
            }
        }

        protected virtual void ProcessCommand(object cmd, ActivityExecutionContext executionContext)
        {
            if (cmd is SynchronizeCommand)
            {
                //cancel the currently executing activity
                if (this.SynchronizationMode == SynchronizationMode.NotAllowed)
                {
                    throw new InvalidOperationException(SR.GetString(SR.Error_SyncCommandNotAllowed, this.QualifiedName));
                }

                // TODO: check for HistoryOnly mode.

                if (this.Current != null)
                {
                    SynchronizeTo(executionContext, ((SynchronizeCommand)cmd));
                }
            }
            else
            {
                throw new ArgumentException(SR.GetString(SR.Error_UnknowQueryCommand, cmd.GetType().FullName));
            }
        }

        private bool TryNavigatingTo(ActivityExecutionContext context, Activity previousActivity, Bookmark syncToBookmark) 
        {
            // We first give a chance to the workflow developer to over-write the target
            // to which we'll execute next.
            NavigatorEventArgs nargs = new NavigatorEventArgs(previousActivity);
            this.RaiseGenericEvent(Navigator.NavigatingEvent, this, nargs);

            UserInputValidationException error = null;
            Activity current = this.Current;
            if (!string.IsNullOrEmpty(nargs.GoTo))
            {
                current = Helpers.GetImmediateChildByName(this, nargs.GoTo);
            }
            else if (syncToBookmark != null)
            {
                if (syncToBookmark.InteractionId == Guid.Empty)
                {
                    current = Helpers.GetImmediateChildByName(this, syncToBookmark.ActivityName);
                }
                else
                {
                    current = FindPreviouslyExecutedActivity(syncToBookmark.InteractionId);
                    if (current == null)
                    {
                        throw new ArgumentException(SR.GetString(SR.Error_HistoryNotFound, syncToBookmark.InteractionId));
                    }

                    Guid currentContextID = (Guid)current.GetValue(Activity.ActivityContextGuidProperty);
                    RemoveHistory(currentContextID, this, true);
                }
            }
            else
            {
                try
                {
                    string nextActivity = GetNavigateTo(current, context) as string;
                    if (!string.IsNullOrEmpty(nextActivity))
                    {
                        current = Helpers.GetImmediateChildByName(this, nextActivity);
                    }
                    else
                    {
                        current = null;
                    }
                }
                catch (TargetInvocationException tiException)
                {
                    if (tiException.InnerException is UserInputValidationException)
                    {
                        error = (UserInputValidationException)tiException.InnerException;
                    }
                    else
                    {
                        throw tiException;
                    }
                }

            }

            if (current != null)
            {
                if (error != null && current is InteractionActivity)
                {
                    ((InteractionActivity)current).Error = error;
                }

                ActivityExecutionContext innerContext = context.ExecutionContextManager.CreateExecutionContext(current);
                innerContext.Activity.RegisterForStatusChange(Activity.ClosedEvent, this);
                innerContext.ExecuteActivity(innerContext.Activity);
                this.Current = innerContext.Activity;

                // Add the current activity to the history list.  We need to do this so we can go back to an 
                // InteractionGroupActivity from its child.
                if (!(innerContext.Activity is Navigator))
                {
                    AddHistory(innerContext.Activity);
                }

                return true;
            }

            this.Current = null;
            return false;
        }

        internal void SynchronizeTo(ActivityExecutionContext executionContext, SynchronizeCommand syncCommand)
        {
            if (executionContext == null)
            {
                throw new ArgumentNullException("executionContext");
            }

            if (syncCommand == null)
            {
                throw new ArgumentNullException("syncCommand");
            }

            bool cancelCurrent = true;
            if (this.Current is InteractionBaseActivity)
            {
                InteractionContext currentInteraction = ((InteractionBaseActivity)this.Current).GetInteractionContext();
                if (currentInteraction.Bookmark.InteractionId == syncCommand.Bookmark.InteractionId)
                {
                    cancelCurrent = false;
                    syncCommand.NextInteraction = currentInteraction;
                    Guid currentContextID = (Guid)this.Current.GetValue(Activity.ActivityContextGuidProperty);
                    RemoveHistory(currentContextID, this, true);
                    AddHistory(this.Current);
                }
            }

            if (cancelCurrent)
            {
                // Cancel the current child.
                ActivityExecutionContext innerContext = executionContext.ExecutionContextManager.GetExecutionContext(this.Current);
                if (innerContext == null)
                {
                    throw new NullReferenceException("innerContext");
                }

                this.syncToBookmark = syncCommand.Bookmark;
                innerContext.CancelActivity(innerContext.Activity);
            }
        }

        internal static void RemoveHistory(Guid activityContextID, Activity activity, bool clearForwardEntries)
        {
            Activity root = Helpers.GetRootActivity(activity);
            List<Guid> historyContextIDs = root.GetValue(HistoryContextIDsProperty) as List<Guid>;
            Debug.Assert(historyContextIDs != null);
            List<Activity> history = root.GetValue(HistoryProperty) as List<Activity>;

            int index = historyContextIDs.IndexOf(activityContextID);
            if (index >= 0 && index < historyContextIDs.Count)
            {
                if (clearForwardEntries)
                {
                    historyContextIDs.RemoveRange(index, historyContextIDs.Count - index);
                    if (history != null)
                    {
                        history.RemoveRange(index, history.Count - index);
                    }
                }
                else
                {
                    historyContextIDs.RemoveAt(index);
                    if (history != null)
                    {
                        history.RemoveAt(index);
                    }
                }
            }
        }

        internal static void AddHistory(Activity activity)
        {
            Activity root = Helpers.GetRootActivity(activity);
            List<Guid> historyContextIDs = root.GetValue(HistoryContextIDsProperty) as List<Guid>;
            if (historyContextIDs == null)
            {
                historyContextIDs = new List<Guid>();
                root.SetValue(HistoryContextIDsProperty, historyContextIDs);
            }

            Navigator parentNavigator = GetParentNavigator(activity);
            Debug.Assert(parentNavigator != null);
            List<Guid> myChildContextIDs = parentNavigator.GetValue(ChildContextIDsProperty) as List<Guid>;
            Debug.Assert(myChildContextIDs != null);

            Guid activityContextId = (Guid)activity.GetValue(Activity.ActivityContextGuidProperty);
            historyContextIDs.Add(activityContextId);
            myChildContextIDs.Add(activityContextId);

            List<Activity> history = root.GetValue(HistoryProperty) as List<Activity>;
            if (history != null)
            {
                history.Add(activity);
            }
        }

        internal static Navigator GetParentNavigator(Activity activity)
        {
            Navigator parentNavigator = null;

            Activity parent = activity.Parent;
            while (parent != null)
            {
                if (parent is Navigator)
                {
                    parentNavigator = parent as Navigator;
                    break;
                }

                parent = parent.Parent;
            }

            return parentNavigator;
        }

        void CloseNavigator(ActivityExecutionContext context)
        {
            List<Guid> childContextIDs = this.GetValue(ChildContextIDsProperty) as List<Guid>;
            foreach (Guid childContextID in childContextIDs)
            {
                RemoveHistory(childContextID, this, false);
            }

            context.CloseActivity();
        }

        // This function evaluates each Transition.  The first one that evaluates to true become the next state.
        string GetNavigateTo(Activity activity, ActivityExecutionContext context)
        {
            string navigateTo = null;
            TransitionCollection transitions = activity.GetValue(Navigator.TransitionsProperty) as TransitionCollection;
            if (transitions != null && transitions.Count > 0)
            {
                foreach (Transition transition in transitions)
                {
                    navigateTo = transition.Evaluate(activity, context);
                    if (!string.IsNullOrEmpty(navigateTo))
                    {
                        break;
                    }
                }
            }
            return navigateTo;
        }

        Activity FindPreviouslyExecutedActivity(Guid interactionId)
        {
            Activity root = Helpers.GetRootActivity(this);
            List<Activity> historyList = root.GetValue(HistoryProperty) as List<Activity>;
            Activity activity = null;
            foreach (Activity history in historyList)
            {
                if (history is InteractionBaseActivity && ((InteractionBaseActivity)history).InteractionId == interactionId)
                {
                    activity = history;
                    break;
                }
                else if (history is CompositeActivity)
                {
                    foreach (Activity child in Helpers.GetAllChildActivities(history))
                    {
                        if (child is InteractionBaseActivity && ((InteractionBaseActivity)child).InteractionId == interactionId)
                        {
                            activity = history;
                            break;
                        }
                    }
                }
            }

            return activity;
        }

        void InitializeChildPermission()
        {
            foreach (Activity child in this.EnabledActivities)
            {
                InteractionBaseActivity interactionActivity = child as InteractionBaseActivity;
                if (interactionActivity != null && interactionActivity.RequiredPermission == null)
                {
                    interactionActivity.RequiredPermission = this.OwnerPermission;
                }
            }
        }
    }
}

